// 版权归Go作者所有。版权所有。
// 此源代码的使用受BSD样式的约束
// 可以在许可证文件中找到的许可证。

// 包txtar实现了一种简单的基于文本的文件归档格式。
// None
// 该格式的目标是：
// None
// -要足够简单，可以手工创建和编辑。
// -能够存储描述go命令测试用例的文本文件树。
// -在git历史记录和代码审查中表现出色。
// None
// 非目标包括成为一种完全通用的归档格式，
// 存储二进制数据，存储文件模式，存储特殊文件，如
// 符号链接等。
// None
// Txtar格式
// None
// txtar归档是零行或多行注释，然后是一系列文件条目。
// 每个文件条目都以文件标记行开头，格式为“-FILENAME--”
// 然后是构成文件数据的零行或多行文件内容。
// 注释或文件内容在下一个文件标记行结束。
// 文件标记行必须以三字节序列“-”开头
// 并以三字节序列“-”结尾，但
// 文件名周围可以有额外的空白，
// 所有这些都被剥离了。
// None
// 如果txtar文件在最后一行缺少尾随换行符，
// 解析器应该考虑最后的换行符。
// None
// txtar存档中没有可能的语法错误。
package txtar

import (
	"bytes"
	"fmt"
	"os"
	"strings"
)

// 归档是文件的集合。
type Archive struct {
	Comment []byte
	Files   []File
}

// 文件是存档中的单个文件。
type File struct {
	Name string // 文件名（“foo/bar.txt”）
	Data []byte // 文件的文本内容
}

// Format返回存档的序列化形式。
// 假定存档数据结构格式良好：
// a、 注释和所有a.File[i]。数据不包含文件标记行，
// 并且所有a.File[i].Name均为非空。
func Format(a *Archive) []byte {
	var buf bytes.Buffer
	buf.Write(fixNL(a.Comment))
	for _, f := range a.Files {
		fmt.Fprintf(&buf, "-- %s --\n", f.Name)
		buf.Write(fixNL(f.Data))
	}
	return buf.Bytes()
}

// ParseFile将命名文件解析为存档文件。
func ParseFile(file string) (*Archive, error) {
	data, err := os.ReadFile(file)
	if err != nil {
		return nil, err
	}
	return Parse(data), nil
}

// Parse解析存档的序列化形式。
// 返回的归档文件保存数据片段。
func Parse(data []byte) *Archive {
	a := new(Archive)
	var name string
	a.Comment, name, data = findFileMarker(data)
	for name != "" {
		f := File{name, nil}
		f.Data, name, data = findFileMarker(data)
		a.Files = append(a.Files, f)
	}
	return a
}

var (
	newlineMarker = []byte("\n-- ")
	marker        = []byte("-- ")
	markerEnd     = []byte(" --")
)

// findFileMarker在数据中查找下一个文件标记，
// 提取文件名，并返回标记前的数据，
// 文件名和标记后的数据。
// 如果没有下一个标记，findFileMarker将返回before=fixNL（数据），name=“”，after=nil。
func findFileMarker(data []byte) (before []byte, name string, after []byte) {
	var i int
	for {
		if name, after = isMarker(data[i:]); name != "" {
			return data[:i], name, after
		}
		j := bytes.Index(data[i:], newlineMarker)
		if j < 0 {
			return fixNL(data), "", nil
		}
		i += j + 1 // 定位在新的可能标记的开始处
	}
}

// isMarker检查数据是否以文件标记行开始。
// 如果是，则返回该行的名称和该行后面的数据。
// 否则，它返回name==“”并在后面加上未指定的后缀。
func isMarker(data []byte) (name string, after []byte) {
	if !bytes.HasPrefix(data, marker) {
		return "", nil
	}
	if i := bytes.IndexByte(data, '\n'); i >= 0 {
		data, after = data[:i], data[i+1:]
	}
	if !bytes.HasSuffix(data, markerEnd) {
		return "", nil
	}
	return strings.TrimSpace(string(data[len(marker) : len(data)-len(markerEnd)])), after
}

// 如果数据为空或以\n结尾，fixNL将返回数据。
// 否则，fixNL将返回一个新的切片，该切片由添加了最终\n的数据组成。
func fixNL(data []byte) []byte {
	if len(data) == 0 || data[len(data)-1] == '\n' {
		return data
	}
	d := make([]byte, len(data)+1)
	copy(d, data)
	d[len(data)] = '\n'
	return d
}
